เรียนรู้วิธีการใช้ React Error Boundaries ร่วมกับ Hooks เพื่อจัดการข้อผิดพลาดในการโหลดทรัพยากรอย่างราบรื่น ช่วยปรับปรุงประสบการณ์ผู้ใช้และความเสถียรของแอปพลิเคชัน
การโหลดทรัพยากรที่แข็งแกร่งใน React: การจัดการ Error Boundaries ด้วย Hooks อย่างเชี่ยวชาญ
ในเว็บแอปพลิเคชันสมัยใหม่ การโหลดทรัพยากรแบบอะซิงโครนัส (asynchronous) เป็นเรื่องปกติ ไม่ว่าจะเป็นการดึงข้อมูลจาก API, การโหลดรูปภาพ, หรือการนำเข้าโมดูล การจัดการข้อผิดพลาดที่อาจเกิดขึ้นระหว่างการโหลดทรัพยากรเป็นสิ่งสำคัญอย่างยิ่งเพื่อประสบการณ์การใช้งานที่ราบรื่น React Error Boundaries เป็นกลไกที่ช่วยดักจับข้อผิดพลาด JavaScript ที่เกิดขึ้นใน child component tree, บันทึกข้อผิดพลาดเหล่านั้น, และแสดง UI สำรอง (fallback UI) แทนที่จะทำให้แอปพลิเคชันทั้งหมดล่ม บทความนี้จะสำรวจวิธีการใช้ Error Boundaries ร่วมกับ React Hooks อย่างมีประสิทธิภาพเพื่อจัดการข้อผิดพลาดในการโหลดทรัพยากร
ทำความเข้าใจ Error Boundaries
ก่อน React 16 ข้อผิดพลาด JavaScript ที่ไม่ได้รับการจัดการระหว่างการเรนเดอร์คอมโพเนนต์จะทำให้สถานะภายในของ React เสียหายและก่อให้เกิดข้อผิดพลาดที่เข้าใจยากในการเรนเดอร์ครั้งต่อไป Error Boundaries เข้ามาแก้ปัญหานี้โดยทำหน้าที่เป็นบล็อกสำหรับดักจับข้อผิดพลาดทั้งหมดที่เกิดขึ้นใน child components ของมัน โดย Error Boundaries คือ React components ที่ implement lifecycle method อย่างน้อยหนึ่งในสองวิธีต่อไปนี้:
static getDerivedStateFromError(error): static method นี้จะถูกเรียกหลังจากที่ descendant component เกิดข้อผิดพลาด มันจะได้รับข้อผิดพลาดที่เกิดขึ้นเป็น argument และคืนค่าเพื่ออัปเดต state ของคอมโพเนนต์componentDidCatch(error, info): lifecycle method นี้จะถูกเรียกหลังจากที่ descendant component เกิดข้อผิดพลาด มันจะได้รับข้อผิดพลาดที่เกิดขึ้นเป็น argument พร้อมกับ object ที่มีข้อมูลเกี่ยวกับคอมโพเนนต์ที่ทำให้เกิดข้อผิดพลาด คุณสามารถใช้มันเพื่อบันทึกข้อมูลข้อผิดพลาดได้
สิ่งสำคัญคือ Error Boundaries จะดักจับข้อผิดพลาดเฉพาะในขั้นตอน rendering, ใน lifecycle methods, และใน constructors ของ tree ทั้งหมดที่อยู่ภายใต้มันเท่านั้น โดยจะไม่ดักจับข้อผิดพลาดสำหรับ:
- Event handlers (เรียนรู้เพิ่มเติมในหัวข้อด้านล่าง)
- โค้ดแบบอะซิงโครนัส (เช่น callbacks ของ
setTimeoutหรือrequestAnimationFrame) - การเรนเดอร์ฝั่งเซิร์ฟเวอร์ (Server-side rendering)
- ข้อผิดพลาดที่เกิดขึ้นในตัว Error Boundary เอง (แทนที่จะเป็น children ของมัน)
Error Boundaries และ React Hooks: การผสมผสานที่ทรงพลัง
แม้ว่าโดยปกติแล้วเราจะใช้ class components ในการสร้าง Error Boundaries แต่ React Hooks ก็มีแนวทางที่กระชับและเป็น functional มากกว่า เราสามารถสร้าง useErrorBoundary hook ที่ใช้ซ้ำได้ ซึ่งจะห่อหุ้มตรรกะการจัดการข้อผิดพลาดและเป็นวิธีที่สะดวกในการครอบคอมโพเนนต์ที่อาจเกิดข้อผิดพลาดระหว่างการโหลดทรัพยากร
การสร้าง useErrorBoundary Hook แบบกำหนดเอง
นี่คือตัวอย่างของ useErrorBoundary hook:
import { useState, useCallback } from 'react';
function useErrorBoundary() {
const [error, setError] = useState(null);
const resetError = useCallback(() => {
setError(null);
}, []);
const captureError = useCallback((e) => {
setError(e);
}, []);
const ErrorBoundary = useCallback(({ children, fallback }) => {
if (error) {
return fallback ? fallback : An error occurred: {error.message || String(error)};
}
return children;
}, [error]);
return { ErrorBoundary, captureError, error, resetError };
}
export default useErrorBoundary;
คำอธิบาย:
useState: เราใช้useStateเพื่อจัดการสถานะข้อผิดพลาด โดยค่าเริ่มต้นจะตั้งค่า error เป็นnulluseCallback: เราใช้useCallbackเพื่อ memoize ฟังก์ชันresetErrorและcaptureErrorซึ่งเป็นแนวทางปฏิบัติที่ดีเพื่อป้องกันการ re-render ที่ไม่จำเป็นหากฟังก์ชันเหล่านี้ถูกส่งต่อเป็น propsErrorBoundaryComponent: นี่คือ functional component ที่สร้างขึ้นด้วยuseCallbackซึ่งรับchildrenและfallbackprop (เป็น optional) หากมีข้อผิดพลาดอยู่ใน state มันจะเรนเดอร์fallbackcomponent ที่ระบุมาหรือข้อความแสดงข้อผิดพลาดเริ่มต้น มิฉะนั้นจะเรนเดอร์ children คอมโพเนนต์นี้ทำหน้าที่เป็น Error Boundary ของเรา dependency array `[error]` ช่วยให้มั่นใจว่ามันจะ re-render เมื่อ state `error` เปลี่ยนแปลงcaptureErrorFunction: ฟังก์ชันนี้ใช้เพื่อตั้งค่าสถานะข้อผิดพลาด คุณจะต้องเรียกใช้ฟังก์ชันนี้ภายในบล็อกtry...catchเมื่อทำการโหลดทรัพยากรresetErrorFunction: ฟังก์ชันนี้จะล้างสถานะข้อผิดพลาด ทำให้คอมโพเนนต์สามารถ re-render children ของมันได้ (ซึ่งอาจเป็นการพยายามโหลดทรัพยากรใหม่อีกครั้ง)
การนำไปใช้กับการโหลดทรัพยากรพร้อมการจัดการข้อผิดพลาด
ตอนนี้ เรามาดูกันว่าจะใช้ hook นี้เพื่อจัดการข้อผิดพลาดในการโหลดทรัพยากรได้อย่างไร ลองพิจารณาคอมโพเนนต์ที่ดึงข้อมูลผู้ใช้จาก API:
import React, { useState, useEffect } from 'react';
import useErrorBoundary from './useErrorBoundary';
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const { ErrorBoundary, captureError, error, resetError } = useErrorBoundary();
useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch(`https://api.example.com/users/${userId}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
setUser(data);
} catch (e) {
captureError(e);
}
};
fetchData();
}, [userId, captureError]);
if (error) {
return (
Failed to load user data. {user.name}
Email: {user.email}
{/* Other user details */}คำอธิบาย:
- เรา import
useErrorBoundaryhook เข้ามา - เราเรียกใช้ hook เพื่อรับ
ErrorBoundarycomponent, ฟังก์ชันcaptureError, stateerror, และฟังก์ชันresetError - ภายใน
useEffecthook เราครอบการเรียก API ด้วยบล็อกtry...catch - หากเกิดข้อผิดพลาดระหว่างการเรียก API เราจะเรียก
captureError(e)เพื่อตั้งค่าสถานะข้อผิดพลาด - ถ้า state
errorถูกตั้งค่า เราจะเรนเดอร์ErrorBoundarycomponent เราส่งfallbackprop แบบกำหนดเองซึ่งจะแสดงข้อความข้อผิดพลาดและปุ่ม "Retry" (ลองใหม่) การคลิกปุ่มนี้จะเรียกresetErrorเพื่อล้างสถานะข้อผิดพลาด ซึ่งจะทำให้เกิดการ re-render และพยายามดึงข้อมูลอีกครั้ง - หากไม่มีข้อผิดพลาดและข้อมูลผู้ใช้ถูกโหลดเรียบร้อยแล้ว เราจะเรนเดอร์รายละเอียดโปรไฟล์ผู้ใช้
การจัดการข้อผิดพลาดในการโหลดทรัพยากรประเภทต่างๆ
ข้อผิดพลาดในการโหลดทรัพยากรประเภทต่างๆ อาจต้องการกลยุทธ์การจัดการที่แตกต่างกันไป นี่คือสถานการณ์ทั่วไปบางส่วนและวิธีจัดการ:
ข้อผิดพลาดของเครือข่าย (Network Errors)
Network errors เกิดขึ้นเมื่อไคลเอนต์ไม่สามารถเชื่อมต่อกับเซิร์ฟเวอร์ได้ (เช่น เครือข่ายล่มหรือเซิร์ฟเวอร์หยุดทำงาน) ตัวอย่างข้างต้นได้จัดการข้อผิดพลาดพื้นฐานของเครือข่ายโดยใช้ `response.ok` อยู่แล้ว คุณอาจต้องการเพิ่มการตรวจจับข้อผิดพลาดที่ซับซ้อนมากขึ้น ตัวอย่างเช่น:
//Inside the fetchData function
try {
const response = await fetch(`https://api.example.com/users/${userId}`);
if (!response.ok) {
// Consider adding specific error code handling
if (response.status === 404) {
throw new Error("User not found");
} else if (response.status >= 500) {
throw new Error("Server error. Please try again later.");
} else {
throw new Error(`HTTP error! status: ${response.status}`);
}
}
const data = await response.json();
setUser(data);
} catch (error) {
if (error.message === 'Failed to fetch') {
// Likely a network error
captureError(new Error('Network error. Please check your internet connection.'));
} else {
captureError(error);
}
}
ในกรณีนี้ คุณสามารถแสดงข้อความแก่ผู้ใช้เพื่อแจ้งว่ามีปัญหาการเชื่อมต่อเครือข่ายและแนะนำให้พวกเขาตรวจสอบการเชื่อมต่ออินเทอร์เน็ต
ข้อผิดพลาดจาก API (API Errors)
API errors เกิดขึ้นเมื่อเซิร์ฟเวอร์ส่งคืนการตอบกลับที่เป็นข้อผิดพลาด (เช่น 400 Bad Request หรือ 500 Internal Server Error) ดังที่แสดงไว้ข้างต้น คุณสามารถตรวจสอบ `response.status` และจัดการข้อผิดพลาดเหล่านี้ได้อย่างเหมาะสม
ข้อผิดพลาดในการแปลงข้อมูล (Data Parsing Errors)
Data parsing errors เกิดขึ้นเมื่อการตอบกลับจากเซิร์ฟเวอร์ไม่ได้อยู่ในรูปแบบที่คาดไว้และไม่สามารถแปลงข้อมูลได้ (เช่น JSON ไม่ถูกต้อง) คุณสามารถจัดการข้อผิดพลาดเหล่านี้ได้โดยการครอบการเรียก response.json() ด้วยบล็อก try...catch:
//Inside the fetchData function
try {
const response = await fetch(`https://api.example.com/users/${userId}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
setUser(data);
} catch (error) {
if (error instanceof SyntaxError) {
captureError(new Error('Failed to parse data from server.'));
} else {
captureError(error);
}
}
ข้อผิดพลาดในการโหลดรูปภาพ
สำหรับการโหลดรูปภาพ คุณสามารถใช้ event handler onError บนแท็ก <img>:
function MyImage({ src, alt }) {
const { ErrorBoundary, captureError } = useErrorBoundary();
const [imageLoaded, setImageLoaded] = useState(false);
const handleImageLoad = () => {
setImageLoaded(true);
};
const handleImageError = (e) => {
captureError(new Error(`Failed to load image: ${src}`));
};
return (
Failed to load image.